// vPresent is an immersive presentation creation and display application.
// Copyright (C) 2007-2009 by Priority 5 Holdings, Inc.
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation; either version 2 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

// Simple viewer application (with lots of options)
// - Allows connection to server if requested

#include <OpenSG/OSGGLUT.h>
#include <OpenSG/OSGConfig.h>
#include <OpenSG/OSGSimpleGeometry.h>
#include <OpenSG/OSGGLUTWindow.h>
#include <OpenSG/OSGSimpleSceneManager.h>
#include <OpenSG/OSGAction.h>
#include <OpenSG/OSGMaterialGroup.h>
#include <OpenSG/OSGChunkMaterial.h>
#include <OpenSG/OSGSceneFileHandler.h>
#include <OpenSG/OSGShearedStereoCameraDecorator.h>
#include <OpenSG/OSGMultiDisplayWindow.h>
#include <OpenSG/OSGSolidBackground.h>

#include <boost/optional/optional.hpp>
#include <boost/program_options.hpp>
namespace po = boost::program_options;
#include <boost/algorithm/string.hpp>
#include <boost/lexical_cast.hpp>


// The SimpleSceneManager to manage simple applications
OSG::SimpleSceneManager*    gMgr;
OSG::NodePtr                gScene;    // The scene we are viewing
bool                         gUseCluster;
bool                 gUseDebugMode;
OSG::MultiDisplayWindowPtr  gMultiWindow;
OSG::RenderAction*          gRenderAction;

// Position where viewer starts.  Needed for reset.
OSG::Pnt3f gStartFrom;
OSG::Pnt3f gStartAt;
OSG::Vec3f gStartUp;
float       gStartDist;


OSG::ShearedStereoCameraDecoratorPtr lCameraDecorator;
OSG::ShearedStereoCameraDecoratorPtr rCameraDecorator;

// Declarations
int setupGLUT( int *argc, char *argv[] );
void loadScene(std::string file);


// Main function
// - Load scene and get everything up and running
int main(int argc, char* argv[])
{
   std::string scene_file("");
   gUseCluster = false;
   gUseDebugMode = false;

   std::string connection_type;           // Connection type for clustering
   std::string connection_interface;      // Connection interface for clustering
   unsigned h_count(1), v_count(0);       // Counts for clustering
   float eye_separation(0.06f);
   float z_parallax(2.0f);
   bool use_stereo(false);
   std::vector<std::string> servers;      // Cluster servers to connect to

   boost::optional<OSG::Pnt3f> initial_from;
   boost::optional<OSG::Pnt3f> initial_at;
   boost::optional<float>      initial_dist;
   boost::optional<float>      initial_motion_factor;
   boost::optional<float>      near_dist, far_dist;


   // ---- ARGUMENT PROCESSING --- //
   try
   {
      po::options_description prog_opts("Options"), cluster_opts("Cluster Options"),
                              viewer_opts("Viewer Options");

      prog_opts.add_options()
         ("help", "produce help message")
     ("debug-mode", "Run in debugging mode.");

      viewer_opts.add_options()
          ("file,f", po::value<std::string>(), "Model file to load")
      ("from", po::value<std::string>(), "Initial from position: x,y,z")
      ("at", po::value<std::string>(), "Initial at position: x,y,z")
      ("dist", po::value<float>(), "Initial distance from 'at' point.")
      ("motion_factor", po::value<float>(), "Initial motion factor.")
      ("near", po::value<float>(), "Near distance.")
      ("far", po::value<float>(), "Far distance.")
      ;


      cluster_opts.add_options()
         ("cluster", "Run in cluster mode")
     ("stereo", "Run in passive stereo mode (top - left eye, bottom - right eye")
         ("type,t", po::value<std::string>()->default_value("StreamSock"),
                     "Communication method to use (StreamSock or Multicast)")
         ("interface,i", po::value<std::string>(),
                     "Set the connection interface to use.")
         ("hcount,x", po::value<unsigned>()->default_value(1), "Horizontal tile count")
         ("vcount,y", po::value<unsigned>()->default_value(0), "Vertical tile count")
         ("eye_separation,e", po::value<float>()->default_value(eye_separation),
                     "The eye separation value to use for stereo.")
         ("z_parallax_dist,z", po::value<float>()->default_value(z_parallax),
                     "The distance to zero parallax.")
         ("servers,s", po::value< std::vector<std::string> >()->multitoken()->composing(),
                     "List of server names (or machine:port) to use for connection")
         ;

      prog_opts.add(viewer_opts).add(cluster_opts);

      po::positional_options_description p;
      p.add("file", -1);

      po::variables_map vm;
      po::store(po::command_line_parser(argc, argv).
                options(prog_opts).positional(p).run(), vm);
      po::notify(vm);

      if ( vm.count("help") > 0 )
      {
         std::cout << prog_opts << std::endl;
         return 1;
      }

      // Viewer options
      if (vm.count("file") > 0)
      {
         scene_file = vm["file"].as<std::string>();
      }

      if (vm.count("dist"))
      { initial_dist = vm["dist"].as<float>(); }

      if(vm.count("motion_factor"))
      { initial_motion_factor = vm["motion_factor"].as<float>(); }

      if(vm.count("near"))
      { near_dist = vm["near"].as<float>(); }

      if(vm.count("far"))
      { far_dist = vm["far"].as<float>(); }

      if(vm.count("from"))
      {
         std::vector<std::string> from_strs;
     boost::split(from_strs, vm["from"].as<std::string>(), boost::is_any_of(","));
     if(from_strs.size() != 3)
     { std::cerr << "Invalid number of params to 'from'." << std::endl; }
     else
     {
        initial_from = OSG::Pnt3f(boost::lexical_cast<float>(from_strs[0]),
                                      boost::lexical_cast<float>(from_strs[1]),
                                      boost::lexical_cast<float>(from_strs[2]));
            std::cout << "Initial from: " << *initial_from << std::endl;
     }
      }

      if(vm.count("at"))
      {
         std::vector<std::string> from_strs;
     boost::split(from_strs, vm["at"].as<std::string>(), boost::is_any_of(","));
     if(from_strs.size() != 3)
     { std::cerr << "Invalid number of params to 'at'." << std::endl; }
     else
     {
        initial_at = OSG::Pnt3f(boost::lexical_cast<float>(from_strs[0]),
                                    boost::lexical_cast<float>(from_strs[1]),
                                    boost::lexical_cast<float>(from_strs[2]));
        std::cout << "Initial at: " << *initial_at << std::endl;
     }
      }

      // Cluster options
      if ( vm.count("cluster") > 0 )
      {
         std::cout << "Cluster mode selected" << std::endl;
         gUseCluster = true;
      }

      if ( vm.count("stereo") > 0 )
      {
         std::cout << "Stereo mode selected" << std::endl;
         use_stereo = true;
      }

      if(vm.count("debug-mode") > 0)
      {
         std::cout << "---- DEBUG MODE --------" << std::endl;
     gUseDebugMode = true;
      }

      if(vm.count("type") > 0)
      {
         connection_type = vm["type"].as<std::string>();
         if(! ( (std::string("StreamSock") == connection_type) || (std::string("Multicast") == connection_type)))
         {
            std::cout << "Invalid connection type: " << connection_type << std::endl;
            connection_type = "StreamSock";
         }
      }

      if(vm.count("interface") > 0)
      {
         connection_interface = vm["interface"].as<std::string>();
      }

      if(vm.count("servers") > 0)
      {
         servers = vm["servers"].as<std::vector<std::string> >();
      }

      h_count = vm["hcount"].as<unsigned>();
      v_count = vm["vcount"].as<unsigned>();
      eye_separation = vm["eye_separation"].as<float>();
      z_parallax = vm["z_parallax_dist"].as<float>();
   }
   catch (std::exception& ex)
   {
      std::cout << ex.what() << std::endl;
      return -1;
   }

   // --- INITIALIZE & SETUP OPENSG --- //
   OSG::ChangeList::setReadWriteDefault();
   OSG::osgInit(argc,argv);

   // Load the scene file
   loadScene(scene_file);

   // GLUT init
   int winid = setupGLUT(&argc, argv);

   // the connection between GLUT and OpenSG
   // XXX: May be able to use this to view locally
   /*
   OSG::GLUTWindowPtr gwin= OSG::GLUTWindow::create();
   gwin->setId(winid);
   gwin->init();
   */

   // Setup the scene manager
   gMgr = new OSG::SimpleSceneManager;

   // If no cluster connection
   // - connect ssm to window and set root
   if(!gUseCluster)
   {
      OSG::GLUTWindowPtr gwin= OSG::GLUTWindow::create();
      gwin->setId(winid);
      gwin->init();

      // Set OpenGL defaults that we expect
      //glDepthFunc( GL_LESS );
      //glEnable(GL_CULL_FACE);
      //glCullFace(GL_BACK);

      //ssm->getAction()->setZWriteTrans(true);
      OSG::RenderAction* rnd_action = dynamic_cast<OSG::RenderAction*>(gMgr->getAction());
      if(NULL == rnd_action)
      {
         std::cerr << "ERROR: Action was NOT render action.\n" << std::endl;
      }
      else
      {
         rnd_action->setZWriteTrans(true);
      }


      gMgr->setWindow(gwin );
      gMgr->setRoot  (gScene);
   }
   // Else
   // - Create cluster window and set it up
   // - Create stereo viewports to put in the cluster window
   else
   {
      std::cout << "Cluster configuration:\n";
      std::cout << "   type:   " << connection_type << std::endl;
      std::cout << "   iface:  " << connection_interface << std::endl;
      std::cout << "   Hcount: " << h_count << std::endl;
      std::cout << "   Vcount: " << v_count << std::endl;
      std::cout << "      eye: " << eye_separation << std::endl;
      std::cout << "   z-dist: " << z_parallax << std::endl;
      std::cout << "   Servers:\n";
      for(unsigned i=0;i<servers.size();++i)
      { std::cout << "      " << servers[i] << std::endl; }

      // --- Setup multi-display --- //
      gMultiWindow = OSG::MultiDisplayWindow::create();
      OSG::CPEditor mwe(gMultiWindow);
      gMultiWindow->setSize(300,300);                 // Dummy size needed for navigator
      //gMultiWindow->setClientWindow(gwin);
      gMultiWindow->setConnectionType(connection_type);
      gMultiWindow->setConnectionInterface(connection_interface);
      for(unsigned i=0;i<servers.size();++i)
      {
         gMultiWindow->getServers().push_back(servers[i]);
      }
      gMultiWindow->setHServers(h_count);
      gMultiWindow->setVServers(v_count);

      gMgr->setWindow(gMultiWindow );
      gMgr->setRoot  (gScene);

      // Left eye decorator
      lCameraDecorator = OSG::ShearedStereoCameraDecorator::create();
      rCameraDecorator = OSG::ShearedStereoCameraDecorator::create();
      OSG::CPEditor lcde(lCameraDecorator), rcde(rCameraDecorator);

      // unit length assume that one unit equals one meter
      lCameraDecorator->setLeftEye(true);
      lCameraDecorator->setEyeSeparation(eye_separation);
      lCameraDecorator->setDecoratee(gMgr->getCamera());
      lCameraDecorator->setZeroParallaxDistance(z_parallax);

      // the right decorator for the right eye
      rCameraDecorator->setLeftEye(false);
      rCameraDecorator->setEyeSeparation(eye_separation);
      rCameraDecorator->setDecoratee(gMgr->getCamera());
      rCameraDecorator->setZeroParallaxDistance(z_parallax);

      // --- Viewports --- //
      OSG::ViewportPtr left_vp(OSG::Viewport::create());
      OSG::ViewportPtr right_vp(OSG::Viewport::create());
      OSG::CPEditor lve(left_vp), rve(right_vp);

      OSG::SolidBackgroundPtr lbkg = OSG::SolidBackground::create();
      OSG::SolidBackgroundPtr rbkg = OSG::SolidBackground::create();
      OSG::CPEditor lbe(lbkg), rbe(rbkg);

      float bkg_color_intensity = (gUseDebugMode ? 0.6f : 0.0f);
      lbkg->setColor(OSG::Color3f(0,bkg_color_intensity,0));
      rbkg->setColor(OSG::Color3f(bkg_color_intensity,0,0));

      // Setup viewports with top half of screen with a left eye
      left_vp->setCamera    (lCameraDecorator);
      left_vp->setBackground(lbkg);
      left_vp->setRoot      (gMgr->getRoot()->getParent());
      left_vp->setSize      (0,0.5,1,1);    // left, bottom, right, top

      // And bottom half os screen with right eye
      right_vp->setCamera(rCameraDecorator);
      right_vp->setBackground(rbkg);
      right_vp->setRoot(gMgr->getRoot()->getParent());
      right_vp->setSize(0,0,1,0.5);

      if(use_stereo)
      {
         gMultiWindow->getPort().clear();
         gMultiWindow->addPort(left_vp);
         gMultiWindow->addPort(right_vp);
      }
      gMultiWindow->init();
   }

   gMgr->showAll();
   gMgr->setStatistics(false);

   // Check for initial settings
   OSG::Navigator* nav = gMgr->getNavigator();
   if(initial_from) nav->setFrom(*initial_from);
   if(initial_at) nav->setAt(*initial_at);
   if(initial_dist) nav->setDistance(*initial_dist);
   if(initial_motion_factor) nav->setMotionFactor(*initial_motion_factor);

   // ???  If only dirk was here.  He would know what to type???
   OSG::beginEditCP(gMgr->getCamera());
   if(near_dist) gMgr->getCamera()->setNear(*near_dist);
   if(far_dist) gMgr->getCamera()->setFar(*far_dist);
   OSG::endEditCP(gMgr->getCamera());

   // Store values for reset
   gStartFrom = gMgr->getNavigator()->getFrom();
   gStartAt = gMgr->getNavigator()->getAt();
   gStartUp = gMgr->getNavigator()->getUp();
   gStartDist = gMgr->getNavigator()->getDistance();

   // GLUT main loop
   glutMainLoop();

   return 0;
}


void loadScene(std::string file)
{
   if(file.empty())
   {
      std::cout << "WARNING: No file specified." << std::endl;
      gScene = OSG::makeTorus(.5, 2, 16, 16);
   }
   else
   {
      gScene = OSG::SceneFileHandler::the().read(file.c_str());
   }
}

//
// GLUT callback functions
//

// redraw the window
void display(void)
{
   gMgr->idle();
   gMgr->redraw();

   OSG::Thread::getCurrentChangeList()->clearAll();
}

// react to size changes
void reshape(int w, int h)
{
    gMgr->resize(w, h);
    glutPostRedisplay();
}

// react to mouse button presses
void mouse(int button, int state, int x, int y)
{
    if (state)
    { gMgr->mouseButtonRelease(button, x, y); }
    else
    { gMgr->mouseButtonPress(button, x, y); }

    glutPostRedisplay();
}

// react to mouse motions with pressed buttons
void motion(int x, int y)
{
    gMgr->mouseMove(x, y);
    glutPostRedisplay();
}

// react to keys
void keyboard(unsigned char k, int , int )
{
   static bool stats_on(false);
   static bool headlight_on(true);

   switch(k)
   {
   case 27:
   {
      OSG::osgExit();
      exit(0);
   }
   break;

   case 'f':
   {
      gMgr->setNavigationMode(OSG::Navigator::FLY);
   }
   break;

   case 't':
   {
      gMgr->setNavigationMode(OSG::Navigator::TRACKBALL);
   }
   break;

   case 'l':
   case 'L':
   {
      headlight_on = !headlight_on;
      if(headlight_on) { gMgr->turnHeadlightOn(); }
      else { gMgr->turnHeadlightOff(); }
      std::cout << "Headlight: " << headlight_on << std::endl;
   }
   break;

   case 'r':
   case 'R':
   {
      gMgr->getNavigator()->set(gStartFrom, gStartAt, gStartUp);
      gMgr->getNavigator()->setDistance(gStartDist);
      std::cout << "Reseting to initial position." << std::endl;
   }
   break;

   case 's':
   case 'S':
   {
      stats_on = !stats_on;
      gMgr->setStatistics(stats_on);
      std::cout << "Stats: " << stats_on << std::endl;
   }
   break;

   case 'z':
   case 'Z':
   {
      OSG::CPEditor lcde(lCameraDecorator), rcde(rCameraDecorator);
      float z_inc(0.01f);
      if( 'z' == k)
      { z_inc *= -1.0f; }

      float new_zero_p = lCameraDecorator->getZeroParallaxDistance() + z_inc;
      lCameraDecorator->setZeroParallaxDistance(new_zero_p);
      rCameraDecorator->setZeroParallaxDistance(new_zero_p);
      std::cout << "New z parallax dist:: " << new_zero_p << std::endl;
   }
   break;

   case 'e':
   case 'E':
   {
      OSG::CPEditor lcde(lCameraDecorator), rcde(rCameraDecorator);
      float sep_inc(0.01f);
      if( 's' == k)
      { sep_inc *= -1.0f; }

      float new_sep = lCameraDecorator->getEyeSeparation() + sep_inc;
      lCameraDecorator->setEyeSeparation(new_sep);
      rCameraDecorator->setEyeSeparation(new_sep);
      std::cout << "New separation: " << new_sep << std::endl;
   }
   break;

   case 'h':
   {
      std::cout << "---- Keys ----" << std::endl
                << "s - Toggle stats.\n"
                << "l - Toggle headlight.\n"
                << "r - Reset to start position.\n"
                << "f/t - Fly/trackball mode.\n"
                << "z/Z - Adjust zero parallax dist.\n"
                << "e/E - Adjust eye separation.\n"
                << std::endl;
   }
   break;

   case '?':
   {
    std::cout << "--- Viewer Status ----" << std::endl;
    OSG::Navigator* nav = gMgr->getNavigator();
    std::cout << "Nav:\n"
              << "   from:" << nav->getFrom() << std::endl
          << "     at:" << nav->getAt() << std::endl
          << "     up:" << nav->getUp() << std::endl
          << "   dist:" << nav->getDistance() << std::endl
          << "  rot a:" << nav->getRotationAngle() << std::endl
          << " m fact:" << nav->getMotionFactor() << std::endl
                  << "   near:" << gMgr->getCamera()->getNear() << std::endl
                  << "    far:" << gMgr->getCamera()->getFar() << std::endl;
    std::cout << "----------------------" << std::endl;
   }
   break;
   }
}


// setup the GLUT library which handles the windows for us
int setupGLUT(int *argc, char *argv[])
{
    glutInit(argc, argv);
    glutInitDisplayMode(GLUT_RGB | GLUT_DEPTH | GLUT_DOUBLE);

    int winid = glutCreateWindow("OpenSG Viewer");

    glutReshapeFunc(reshape);
    glutDisplayFunc(display);
    glutIdleFunc(display);
    glutMouseFunc(mouse);
    glutMotionFunc(motion);
    glutKeyboardFunc(keyboard);

    return winid;
}
